输入和输出

Java的IO包主要关注的是从原始数据源的读取以及输出原始数据到目标媒介。以下是最典型的数据源和目标媒介:

  • 文件
  • 管道
  • 网络连接
  • 内存缓存
  • System.in, System.out, System.error(注:Java标准输入、输出、错误输出)

下面这张图描绘了一个程序从数据源读取数据,然后将数据输出到其他媒介的原理:

Source -> program -> Destination

流是一个核心的概念。流从概念上来说是一个连续的数据流。既可以从流中读取数据,也可以往流中写数据。流与数据源或者数据流向的媒介相关联。在Java IO中流既可以是字节流(以字节为单位进行读写),也可以是字符流(以字符为单位进行读写)。

一个程序需要InputStream或者Reader从数据源读取数据,需要OutputStream或者Writer将数据写入到目标媒介中。以下的图说明了这一点:

流通常会与数据源、数据流向目的地相关联,比如文件、网络等等。

流和数组不一样,不能通过索引读写数据。在流中,你也不能像数组那样前后移动读取数据,除非使用RandomAccessFile 处理文件。流仅仅只是一个连续的数据流。某些类似PushbackInputStream 流的实现允许你将数据重新推回到流中,以便重新读取。然而你只能把有限的数据推回流中,并且你不能像操作数组那样随意读取数据。流中的数据只能够顺序访问。Java IO流通常是基于字节或者基于字符的。字节流通常以“stream”命名,比如InputStream和OutputStream。字符流通常以“Reader”或者“Writer”命名。字符流能够读写字符(比如Latin1或者Unicode字符)。

InputStream

java.io.InputStream类是所有Java IO输入流的基类。如果你正在开发一个从流中读取数据的组件,请尝试用InputStream替代任何它的子类(比如FileInputStream)进行开发。这么做能够让你的代码兼容任何类型而非某种确定类型的输入流。通常使用输入流中的read()方法读取数据。read()方法返回一个整数,代表了读取到的字节的内容(译者注:0 ~ 255)。当达到流末尾没有更多数据可以读取的时候,read()方法返回-1。

1
2
3
4
5
6
7
8
InputStream inputstream = new FileInputStream("c:\\data\\input-text.txt");
byte[] data = new byte[1024];
int bytesRead = inputstream.read(data);
while(bytesRead != -1) {
doSomethingWithData(data, bytesRead);
bytesRead = inputstream.read(data);
}
inputstream.close();

read()方法返回从InputStream流内读取到的一个字节内容(译者注:0~255)。如果read()方法返回-1,意味着程序已经读到了流的末尾,此时流内已经没有多余的数据可供读取了。-1是一个int类型,不是byte或者char类型,这是不一样的。

InputStream包含了2个从InputStream中读取数据并将数据存储到缓冲数组中的read()方法,他们分别是:

1
2
int read(byte b[])
int read(byte b[], int offset, int length)

一次性读取一个字节数组的方式,比一次性读取一个字节的方式快的多,所以,尽可能使用这两个方法代替read()方法。

1
2
3
read(byte[])方法会尝试读取与给定字节数组容量一样大的字节数,返回值说明了已经读取过的字节数。如果InputStream内可读的数据不足以填满字节数组,则数组剩余部分将保留参数传入时的原始数据,记得检查有多少数据实际被写入到了字节数组中,以防读取无效数据。
read(byte, int offset, int length)方法同样将数据读取到字节数组中,不同的是,该方法从数组的offset位置开始,并且最多将length个字节写入到数组中。同样地,read(byte, int offset, int length)方法返回一个int变量,告诉你已经有多少字节已经被写入到字节数组中,所以请记得在读取数据前检查上一次调用read(byte, int offset, int length)的返回值。
这两个方法都会在读取到达到流末尾时返回-1。

OutputStream

java.io.OutputStream是Java IO中所有输出流的基类。如果你正在开发一个能够将数据写入流中的组件,请尝试使用OutputStream替代它的所有子类。

1
2
3
OutputStream output = new FileOutputStream("c:\\data\\output-file.txt");
output.write("Hello World".getBytes());
output.close();
write(byte)

write(byte)方法用于把单个字节写入到输出流中。OutputStream的write(byte)方法将一个包含了待写入数据的int变量作为参数进行写入。只有int类型的第一个字节会被写入,其余位会被忽略。(译者注:写入低8位,忽略高24位)。

OutputStream的子类可能会包含write()方法的替代方法。比如,DataOutputStream允许你利用writeBoolean(),writeDouble()等方法将基本类型int,long,float,double,boolean等变量写入。

1
2
3
4
5
6
OutputStream output = new FileOutputStream("c:\\data\\output-text.txt");
while(hasMoreData()) {
int data = getMoreData();
output.write(data);
}
output.close();
write([byte[])

OutputStream同样包含了将字节数据中全部或者部分数据写入到输出流中的方法,分别是write(byte[])和write(byte[], int offset, int length)。write(byte[])把字节数组中所有数据写入到输出流中。write(byte[], int offset, int length)把字节数据中从offset位置开始,length个字节的数据写入到输出流。

flush()

OutputStream的flush()方法将所有写入到OutputStream的数据冲刷到相应的目标媒介中。比如,如果输出流是FileOutputStream,那么写入到其中的数据可能并没有真正写入到磁盘中。即使所有数据都写入到了FileOutputStream,这些数据还是有可能保留在内存的缓冲区中。通过调用flush()方法,可以把缓冲区内的数据刷新到磁盘(或者网络,以及其他任何形式的目标媒介)中。

组合流

你可以将流整合起来以便实现更高级的输入和输出操作。比如,一次读取一个字节是很慢的,所以可以从磁盘中一次读取一大块数据,然后从读到的数据块中获取字节。为了实现缓冲,可以把InputStream包装到BufferedInputStream中。代码示例:

1
InputStream input = new BufferedInputStream(new FileInputStream("c:\\data\\input-file.txt"));

缓冲同样可以应用到OutputStream中。你可以实现将大块数据批量地写入到磁盘(或者相应的流)中,这个功能由BufferedOutputStream实现。

缓冲只是通过流整合实现的其中一个效果。你可以把InputStream包装到PushbackInputStream中,之后可以将读取过的数据推回到流中重新读取,在解析过程中有时候这样做很方便。或者,你可以将两个InputStream整合成一个SequenceInputStream

将不同的流整合到一个链中,可以实现更多种高级操作。通过编写包装了标准流的类,可以实现你想要的效果和过滤器。

文件

通过IO读文件

如果需要在不同端之间读取文件,可以根据该文件是二进制文件还是文本文件来选择使用FileInputStream或者FileReader。这两个类允许你从文件开始到文件末尾一次读取一个字节或者字符,或者将读取到的字节写入到字节数组或者字符数组。你不必一次性读取整个文件,相反你可以按顺序地读取文件中的字节和字符。如果需要跳跃式地读取文件其中的某些部分,可以使用RandomAccessFile。

通过IO写文件

如果需要在不同端之间进行文件的写入,你可以根据你要写入的数据是二进制型数据还是字符型数据选用FileOutputStream或者FileWriter。你可以一次写入一个字节或者字符到文件中,也可以直接写入一个字节数组或者字符数据。数据按照写入的顺序存储在文件当中。

字节和字符数组

在java中常用字节和字符数组在应用中临时存储数据。而这些数组又是通常的数据读取来源或者写入目的地。如果需要在程序运行时需要大量读取文件里的内容,那么你也可以把一个文件加载到数组中。当然你可以通过直接指定索引来读取这些数组。但如果设计成为从InputStream或者Reader,而不是从数组中读取某些数据的话,会用什么组件呢?

InputStream Reader中读取数组

用ByteArrayInputStream或者CharArrayReader封装字节或者字符数组从数组中读取数据。通过这种方式字节和字符就可以以数组的形式读出了。

1
2
3
4
5
6
7
8
9
10
byte[] bytes = new byte[1024];
//把数据写入字节数组...
InputStream input = new ByteArrayInputStream(bytes);
//读取第一个字节
int data = input.read();
while(data != -1) {
//操作数据
//读下一个字节
data = input.read();
}

以同样的方式也可以用于读取字符数组,只要把字符数组封装在CharArrayReader上就行了。

通过OutputStream 或者Writer写数据

同样,也可以把数据写到ByteArrayOutputStream或者CharArrayWriter中。你只需要创建ByteArrayOutputStream或者CharArrayWriter,把数据写入,就像写其它的流一样。当所有的数据都写进去了以后,只要调用toByteArray()或者toCharArray,所有写入的数据就会以数组的形式返回。

1
2
3
OutputStream output = new ByteArrayOutputStream();
output.write("This text is converted to bytes".toBytes("UTF-8"));
byte[] bytes = output.toByteArray();

System.in System.out,System.err

System.in, System.out, System.err这3个流同样是常见的数据来源和数据流目的地。使用最多的可能是在控制台程序里利用System.out将输出打印到控制台上。

System.in

System.in是一个典型的连接控制台程序和键盘输入的InputStream流。通常当数据通过命令行参数或者配置文件传递给命令行Java程序的时候,System.in并不是很常用。图形界面程序通过界面传递参数给程序,这是一块单独的Java IO输入机制。

System.out

System.out是一个PrintStream流。System.out一般会把你写到其中的数据输出到控制台上。System.out通常仅用在类似命令行工具的控制台程序上。System.out也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。

System.err

System.err是一个PrintStream流。System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。一些类似Eclipse的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过System.err输出到控制台上。

尽管System.in, System.out, System.err这3个流是java.lang.System类中的静态成员(译者注:这3个变量均为final static常量),并且已经预先在JVM启动的时候初始化完成,依然可以更改它们。只需要把一个新的InputStream设置给System.in或者一个新的OutputStream设置给System.out或者System.err,之后的数据都将会在新的流中进行读取、写入。可以使用System.setIn(), System.setOut(), System.setErr()方法设置新的系统流

1
2
3
OutputStream output = new FileOutputStream("c:\\data\\system.out.txt");
PrintStream printOut = new PrintStream(output);
System.setOut(printOut);

现在所有的System.out都将重定向到”c:\data\system.out.txt”文件中。请记住,务必在JVM关闭之前冲刷System.out(译者注:调用flush()),确保System.out把数据输出到了文件中。

Reader 和Writer

Reader类是Java IO中所有Reader的基类。子类包括BufferedReader,PushbackReader,InputStreamReader,StringReader和其他Reader。

Writer类是Java IO中所有Writer的基类。子类包括BufferedWriter和PrintWriter等等。

请注意,InputStream的read()方法返回一个字节,意味着这个返回值的范围在0到255之间(当达到流末尾时,返回-1),Reader的read()方法返回一个字符,意味着这个返回值的范围在0到65535之间(当达到流末尾时,同样返回-1)。这并不意味着Reade只会从数据源中一次读取2个字节,Reader会根据文本的编码,一次读取一个或者多个字节。

整合Reader 和InputStream Writer 和OutputStream

一个Reader可以和一个InputStream相结合。如果你有一个InputStream输入流,并且想从其中读取字符,可以把这个InputStream包装到InputStreamReader中。把InputStream传递到InputStreamReader的构造函数中:

1
Reader reader =new InputStreamReader(inputStream);

与Reader和InputStream类似,一个Writer可以和一个OutputStream相结合。把OutputStream包装到OutputStreamWriter中,所有写入到OutputStreamWriter的字符都将会传递给OutputStream。这是一个OutputStreamWriter的例子:

1
Writer writer = new OutputStreamWriter(outputStream);